Python尋找變數的方法是透過LEGB
,即Local
、Enclosing
、Global
及Built-in
scope,來層層尋找。有興趣深究的朋友,可以參考這篇Real Python的講解。
雖然概念是清楚的,但仍然有一些需要注意的地方。本翼中,我們將分別以L
、E
、G
及B
來代稱Local
、Enclosing
、Global
及Built-in
scope。
UnBoundLocalError
是一個常見的錯誤。# 01
中,當unboundlocalerror_func
被呼叫時,會raise UnboundLocalError
。您可能會覺得疑惑,覺得print(x)
會因為找不到L
的x
,而unboundlocalerror_func
又沒有E
,所以會在G
中來尋找x
,為什麼會報錯呢?
這是因為在unboundlocalerror_func
有x = 2
這個assignment
。Python是在execute(或是想成compile)時就決定一個變數是不是local variable
。由於unboundlocalerror_func
中我們有指定x
為2
,Python於execute(或compile)階段就會先認定x
是一個local variable
。接著當我們真正呼叫unboundlocalerror_func
時,Python會開始依照LEGB
的原則尋找變數。由於x
已被認定是一個local variable
,所以Python只會在unboundlocalerror_func
這個L
中尋找x
,而我們的確於定義x
前,就使用了print(x)
,所以會報錯。
# 01
x = 1
def unboundlocalerror_func():
print(x)
x = 2
unboundlocalerror_func() # UnboundLocalError
在使用各種Comprehension
時,也是一個容易出錯的地方。我們透過一連串小例子,慢慢說明。
# 02a
中,當我們真正呼叫adders
中每一個adder
時,Python會依照LEGB
原則尋找n
。由於在L
找不到n
,又沒有E
,最後在G
中找到n
,其值為3
(不是10
),因為在for n in range(1, 4)
中,n
最後於G
中被指為3
。
# 02a
n = 10
adders = []
for n in range(1, 4):
adders.append(lambda x: x+n)
for adder in adders:
print(adder(1)) # 4 4 4
如果能理解# 02a
的話,# 02b
也是相同概念,最後於G
中的n
值為10
。
# 02b
adders = []
for n in range(1, 4):
adders.append(lambda x: x+n)
n = 10
for adder in adders:
print(adder(1)) # 11 11 11
我們將# 02a
以list comprehension的方式改寫為# 02c
,答案不變。
# 02c
n = 10
adders = [lambda x:x+n for n in range(1, 4)]
for adder in adders:
print(adder(1)) # 4 4 4
但當我們將# 02b
改寫為# 02d
時,答案卻變了,這可能會讓你有點驚訝。
# 02d
adders = [lambda x:x+n for n in range(1, 4)]
n = 10
for adder in adders:
print(adder(1)) # 4 4 4
事實上,comprehension
內就像一個小的local
scope(或可以想成一個namespace
),# 02c
與# 02d
可以改寫為# 02e
,至於n = 10
放前放後,並不影響答案。
# 02e
n = 10
def get_adders():
adders = []
for n in range(1, 4):
def my_func(x):
return x+n
adders.append(my_func)
return adders
adders2 = get_adders()
for adder in adders2:
print(adder(1)) # 4 4 4
當我們呼叫每個adder
時,由於L
中找不到n
,所以我們是在E
這層找到n
,其值為3
,因為for n in range(1, 4)
在最後將n
指為3
。
如果我們想要每個adder
都能獲得不同的n
值,# 02f
是一個可以參考的寫法。我們將n定義為lambda
的keyword argument
,並預設其值為n
。這麼一來,於迴圈中我們就可以接受不同的n
值了。
# 02f
n = 10
adders = [lambda x, n=n:x+n for n in range(1, 4)]
for adder in adders:
print(adder(1)) # 2 3 4
由 class body
內取得變數時,也是一個容易讓人搞糊塗的地方,我們透過# 03
來了解。
# 03
fruit = 'Apple'
class Basket:
fruit = 'Orange'
partition1 = [fruit] * 3
partition2 = [fruit for _ in range(3)]
def get_fruit(self):
return f'{fruit}'
basket = Basket()
print(basket.get_fruit()) # Apple
print(basket.fruit) # Orange
print(Basket.partition1) # ['Orange', 'Orange', 'Orange']
print(basket.partition2) # ['Apple', 'Apple', 'Apple']
basket.get_fruit()
會返回Apple
不是Orange
,因為在function
內,如果要取得class
或instance
的variable
時,要使用類似self
或cls
等語法。所以當呼叫basket.get_fruit()
時,L
找不到fruit
,又沒有E
,所以找到的是G
的Apple
。basket.fruit
會返回Orange
,因為basket.__dict__
中並沒有fruit
,所以basket.fruit
會往上到Basket
中尋找。此時於Basket
有找到fruit
,所以返回其值。Basket.partition1
會返回['Orange', 'Orange', 'Orange']
。Basket.__dict__
中有找到partition1
,所以返回其值。由於partition1
與fruit
同是class variable
,所以[fruit] * 3
實際上是將fruit
其連續三次置入list
中。basket.partition2
會返回['Apple', 'Apple', 'Apple']
。因為basket.__dict__
中並沒有partition2
,所以basket.partition2
會往上到Basket
中尋找。此時於Basket
找到partition2
,所以返回其值。由於partition2
使用list comprehension
,所以相當於其在一個function
底下,情況跟get_fruit
是非常類似的,所以其最後找到的是,也會是G
的Apple
。今天最後,我們來一個metaclasses
與scope
的綜合練習題。
寫一個metaclasses
來生成如TargetClass
的class
。
__init__
接受任意數目的**kwargs
。kwargs
中的key
加上_(underscore)
後,設為instance variable
,其值為原key
所相對應的value
。kwargs
中的key
,建立property
,並返回相對應加上_(underscore)
的instance variable
。# 04
class TargetClass:
def __init__(self, **kwargs):
"""
kwargs: {'x': 1, 'y':2, ...}
"""
self.__dict__.update({f'_{k}': v for k, v in kwargs.items()})
@property
def x(self):
return self._x
@property
def y(self):
return self._y
...
MyType1
繼承type
。MyType1.__new__
中使用super().__new__
生成cls
。init function
,其內部邏輯與上述TargetClass.__init__
相同,並指定給cls.__init__
。kwargs
打個迴圈,將其key
依序設為property
,並返回相對應的底層加上_(underscore)
的instance variable
。cls
。這麼一來我們就可以使用MyType1
作為MyClass1
的metaclass
,並搭配kwds
作為MyType1.__new__
的最後一個參數**kwargs
,來生成MyClass1
,及使用my_inst1 = MyClass1(**kwds)
生成my_inst1
。
# 04
class MyType1(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, **kwargs):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
def init(self, **kwargs):
self.__dict__.update({f'_{k}': v for k, v in kwargs.items()})
cls.__init__ = init
for prop in kwargs:
setattr(cls,
prop,
property(lambda self: getattr(self, f'_{prop}')))
return cls
kwds = {'x': 1, 'y': 2}
class MyClass1(metaclass=MyType1, **kwds):
pass
my_inst1 = MyClass1(**kwds)
...
但是如果觀察my_inst1.x
與my_inst1.y
,卻發現其值都為2
。
>>> vars(my_inst1) # {'_x': 1, '_y': 2}
>>> my_inst1.x, my_inst1.y # 2 2
問題出在property(lambda self: getattr(self, f'_{prop}'))
。lambda self: getattr(self, f'_{prop}'
是一個getter function
,其接收的prop
參數,是由for prop in kwargs
而來。當我們真正使用my_inst1.x
及my_inst1.y
時,每個getter
都會先在L
中找prop
,因為找不到,所以往外找。最後在E
中找到prop
,並認為prop
是kwargs
的最後一個key
。
修正解法是使用keyword arguments
,將property(lambda self: getattr(self, f'_{prop}'))
改為property(lambda self, attr=prop: getattr(self, f'_{attr}'))
。整體思路是與# 02f
類似的,只是因為在metaclass
內,語法比較複雜而已。
# 04
...
class MyType2(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, **kwargs):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
def init(self, **kwargs):
self.__dict__.update({f'_{k}': v for k, v in kwargs.items()})
cls.__init__ = init
for prop in kwargs:
setattr(cls,
prop,
property(lambda self, attr=prop: getattr(self, f'_{attr}')))
return cls
kwds = {'x': 1, 'y': 2}
class MyClass2(metaclass=MyType2, **kwds):
pass
my_inst2 = MyClass2(**kwds)
觀察my_inst2.x
為1
,而my_inst2.y
為2
,皆正確無誤。
>>> vars(my_inst2) # {'_x': 1, '_y': 2}
>>> my_inst2.x, my_inst2.y # 1 2
mutate
,而發生預期外的行為。function
(或lambda
)的參數時,keyword arguments
可能是您的好幫手。Dr. Fred Baptiste
於多個單元中,都曾反覆強調。
牛刀小試
的例題改寫自參考Part 4-Section 14 -Metaprogramming-11 Metaprogramming Application 1。